/*
 * The Unified Mapping Platform (JUMP) is an extensible, interactive GUI 
 * for visualizing and manipulating spatial features with geometry and attributes.
 *
 * Copyright (C) 2003 Vivid Solutions
 * 
 * This program is free software; you can redistribute it and/or
 * modify it under the terms of the GNU General Public License
 * as published by the Free Software Foundation; either version 2
 * of the License, or (at your option) any later version.
 * 
 * This program is distributed in the hope that it will be useful,
 * but WITHOUT ANY WARRANTY; without even the implied warranty of
 * MERCHANTABILITY or FITNESS FOR A PARTICULAR PURPOSE.  See the
 * GNU General Public License for more details.
 * 
 * You should have received a copy of the GNU General Public License
 * along with this program; if not, write to the Free Software
 * Foundation, Inc., 59 Temple Place - Suite 330, Boston, MA  02111-1307, USA.
 * 
 * For more information, contact:
 *
 * Vivid Solutions
 * Suite #1A
 * 2328 Government Street
 * Victoria BC  V8T 5G5
 * Canada
 *
 * (250)385-6040
 * www.vividsolutions.com
 */

package com.vividsolutions.jump.workbench.ui;

import java.awt.BorderLayout;
import java.awt.Color;
import java.awt.Component;
import java.awt.GridBagConstraints;
import java.awt.GridBagLayout;
import java.awt.Insets;
import java.awt.Point;
import java.awt.Rectangle;
import java.awt.event.ActionEvent;
import java.awt.event.ActionListener;
import java.awt.event.MouseAdapter;
import java.awt.event.MouseEvent;
import java.util.ArrayList;
import java.util.Collection;
import java.util.Iterator;

import javax.swing.BorderFactory;
import javax.swing.DefaultCellEditor;
import javax.swing.Icon;
import javax.swing.JButton;
import javax.swing.JCheckBox;
import javax.swing.JComboBox;
import javax.swing.JComponent;
import javax.swing.JLabel;
import javax.swing.JList;
import javax.swing.JPanel;
import javax.swing.JPopupMenu;
import javax.swing.JScrollPane;
import javax.swing.JTable;
import javax.swing.JTextField;
import javax.swing.JToolBar;
import javax.swing.ListCellRenderer;
import javax.swing.SwingUtilities;
import javax.swing.border.Border;
import javax.swing.border.LineBorder;
import javax.swing.event.ListSelectionEvent;
import javax.swing.event.ListSelectionListener;
import javax.swing.event.TableModelEvent;
import javax.swing.event.TableModelListener;
import javax.swing.table.TableCellRenderer;
import javax.swing.table.TableColumn;

import com.vividsolutions.jts.util.Assert;
import com.vividsolutions.jump.I18N;
import com.vividsolutions.jump.feature.AttributeType;
import com.vividsolutions.jump.util.StringUtil;
import com.vividsolutions.jump.workbench.WorkbenchContext;
import com.vividsolutions.jump.workbench.model.CategoryEvent;
import com.vividsolutions.jump.workbench.model.FeatureEvent;
import com.vividsolutions.jump.workbench.model.Layer;
import com.vividsolutions.jump.workbench.model.LayerEvent;
import com.vividsolutions.jump.workbench.model.LayerEventType;
import com.vividsolutions.jump.workbench.model.LayerListener;
import com.vividsolutions.jump.workbench.plugin.AbstractPlugIn;
import com.vividsolutions.jump.workbench.plugin.EnableCheck;
import com.vividsolutions.jump.workbench.plugin.MultiEnableCheck;
import com.vividsolutions.jump.workbench.plugin.PlugIn;
import com.vividsolutions.jump.workbench.plugin.PlugInContext;
import com.vividsolutions.jump.workbench.ui.addremove.AddRemovePanel;
import com.vividsolutions.jump.workbench.ui.cursortool.editing.EditingPlugIn;
import com.vividsolutions.jump.workbench.ui.images.IconLoader;
import com.vividsolutions.jump.workbench.ui.plugin.EditablePlugIn;
import com.vividsolutions.jump.workbench.ui.plugin.FeatureInstaller;

public class SchemaPanel extends JPanel {
    private JPanel jPanel3 = new JPanel();
    private JPanel jPanel1 = new JPanel();
    private GridBagLayout gridBagLayout1 = new GridBagLayout();
    private JLabel statusLabel = new JLabel();
    private Layer layer;
    private Point currentClickPoint;
    private JPopupMenu popupMenu = new JPopupMenu();
    private GridBagLayout gridBagLayout2 = new GridBagLayout();
    private JPanel buttonPanel = new JPanel();
    private JButton applyButton = new JButton();
    private JCheckBox forceInvalidConversionsToNullCheckBox = new JCheckBox();
    private GridBagLayout gridBagLayout3 = new GridBagLayout();
    private JPanel jPanel2 = new JPanel();
    private Border border1;
    private boolean modified = false;
    private ArrayList listeners = new ArrayList();
    private JButton revertButton = new JButton();
    private BorderLayout borderLayout1 = new BorderLayout();
    private WorkbenchToolBar toolBar = new WorkbenchToolBar(null) {
        public JButton addPlugIn(
            Icon icon,
            PlugIn plugIn,
            EnableCheck enableCheck,
            WorkbenchContext workbenchContext) {
            return super.addPlugIn(icon, addCleanUp(plugIn), enableCheck, workbenchContext);
        }

    };

    private void setModel(SchemaTableModel model) {
        table.setModel(model);
        table.getModel().addTableModelListener(new TableModelListener() {
            public void tableChanged(TableModelEvent e) {
                updateComponents();
            }
        });
        //Must init cell editors every time we init model. [Jon Aquino]
        initCellEditors();
        updateComponents();
    }

    public SchemaPanel(final Layer layer, EditingPlugIn editingPlugIn, WorkbenchContext context) {
        editablePlugIn = new EditablePlugIn(editingPlugIn);
        try {
            table.getSelectionModel().addListSelectionListener(new ListSelectionListener() {
                public void valueChanged(ListSelectionEvent e) {
                    updateComponents();
                }
            });
            this.layer = layer;
            jbInit();
            //Call #initPopupMenu and #initToolBar before #setModel, because #setModel
            //calls #updateComponents. [Jon Aquino]
            initPopupMenu(context);
            initToolBar(context);
            setModel(new SchemaTableModel(layer));
            layer.getLayerManager().addLayerListener(new LayerListener() {
                public void categoryChanged(CategoryEvent e) {}

                public void featuresChanged(FeatureEvent e) {}

                public void layerChanged(LayerEvent e) {
                    if (e.getLayerable() != layer) {
                        return;
                    }

                    if (e.getType() == LayerEventType.METADATA_CHANGED) {
                        //If layer becomes editable, apply row striping and remove gridlines,
                        //as recommended in Java Look and Feel Design Guidelines: Advanced Topics [Jon Aquino]
                        updateComponents();
                        repaint();
                    }
                }
            });
        } catch (Exception ex) {
            Assert.shouldNeverReachHere(ex.toString());
        }
    }

    private int[] rowsToActOn() {
        if (table.getSelectedRowCount() > 0) {
            return table.getSelectedRows();
        }
        if (getCurrentClickPoint() != null && table.rowAtPoint(getCurrentClickPoint()) != -1) {
            return new int[] { table.rowAtPoint(getCurrentClickPoint())};
        }
        return new int[] {};
    }

    private EnableCheck basicEnableCheck = new EnableCheck() {
        public String check(JComponent component) {
            if (!layer.isEditable()) {
                return I18N.get("ui.SchemaPanel.layer-must-be-editable");
            }
            if (rowsToActOn().length == 0) {
                return I18N.get("ui.SchemaPanel.at-least-1-row-must-be-selected");
            }
            return null;
        }
    };

    private abstract class MyPlugIn extends AbstractPlugIn {
        public MultiEnableCheck createEnableCheck() {
            return new MultiEnableCheck().add(basicEnableCheck);
        }
        public abstract Icon getIcon();
    }

    private MyPlugIn insertPlugIn = new MyPlugIn() {
        public String getName() {
            return I18N.get("ui.SchemaPanel.insert");
        }
        public Icon getIcon() { return GUIUtil.toSmallIcon(IconLoader.icon("Plus.gif")); }
        public boolean execute(PlugInContext context) throws Exception {
            markAsModified();
            getModel().insertBlankRow(rowsToActOn()[0]);
            return true;
        }
    };

    private MyPlugIn deletePlugIn = new MyPlugIn() {
        public String getName() {
            return I18N.get("ui.SchemaPanel.delete");
        }
        public Icon getIcon() { return GUIUtil.toSmallIcon(IconLoader.icon("Delete.gif")); }        
        public boolean execute(PlugInContext context) throws Exception {
            markAsModified();
            getModel().removeFields(rowsToActOn());
            return true;
        }
    };

    private MyPlugIn moveUpPlugIn = new MyPlugIn() {
        public Icon getIcon() { return GUIUtil.toSmallIcon(IconLoader.icon("VCRUp.gif")); }        
        public String getName() {
            return I18N.get("ui.SchemaPanel.move-field-up");
        }
        public boolean execute(PlugInContext context) throws Exception {
            markAsModified();
            move(AddRemovePanel.itemsToMoveUp(getModel().getFields(), toFields(rowsToActOn())), -1);
            return true;
        }
        public MultiEnableCheck createEnableCheck() {
            return super.createEnableCheck().add(new EnableCheck() {
                public String check(JComponent component) {
                    return min(rowsToActOn()) == 0 ? I18N.get("ui.SchemaPanel.field-is-already-at-the-top") : null;
                    //No corresponding check in moveDownPlugIn because there is no
                    //bottom! (We keep adding rows as necessary) [Jon Aquino]
                }
            });
        }
    };
    private EditablePlugIn editablePlugIn;

    private MyPlugIn moveDownPlugIn = new MyPlugIn() {
        public Icon getIcon() { return GUIUtil.toSmallIcon(IconLoader.icon("VCRDown.gif")); }                
        public String getName() {
            return I18N.get("ui.SchemaPanel.move-field-down");
        }
        public boolean execute(PlugInContext context) throws Exception {
            markAsModified();
            move(
                AddRemovePanel.itemsToMoveDown(getModel().getFields(), toFields(rowsToActOn())),
                1);
            return true;
        }
    };
    private JScrollPane jScrollPane1 = new JScrollPane();
    private JTable table = new JTable();

    private void initToolBar(WorkbenchContext context) {
        toolBar.addPlugIn(
            insertPlugIn.getIcon(),
            insertPlugIn,
            insertPlugIn.createEnableCheck(),
            context);
        toolBar.addPlugIn(
            deletePlugIn.getIcon(),
            deletePlugIn,
            deletePlugIn.createEnableCheck(),
            context);
        toolBar.addPlugIn(
            moveUpPlugIn.getIcon(),
            moveUpPlugIn,
            moveUpPlugIn.createEnableCheck(),
            context);
        toolBar.addPlugIn(
            moveDownPlugIn.getIcon(),
            moveDownPlugIn,
            moveDownPlugIn.createEnableCheck(),
            context);
    }

    private void initPopupMenu(WorkbenchContext context) {
        table.addMouseListener(new MouseAdapter() {
            public void mouseReleased(MouseEvent e) {
                setCurrentClickPoint(e.getPoint());

                if (SwingUtilities.isRightMouseButton(e)) {
                    popupMenu.show(e.getComponent(), e.getX(), e.getY());
                }

            }
        });
        addPopupMenuItem(
            editablePlugIn,
            true,
            null,
            editablePlugIn.createEnableCheck(context), context);
        popupMenu.addSeparator();
        addPopupMenuItem(
            insertPlugIn,
            false,
            insertPlugIn.getIcon(),
            insertPlugIn.createEnableCheck(), context);
        addPopupMenuItem(
            deletePlugIn,
            false,
            deletePlugIn.getIcon(),
            deletePlugIn.createEnableCheck(), context);
        popupMenu.addSeparator();
        addPopupMenuItem(
            moveUpPlugIn,
            false,
            moveUpPlugIn.getIcon(),
            moveUpPlugIn.createEnableCheck(), context);
        addPopupMenuItem(
            moveDownPlugIn,
            false,
            moveDownPlugIn.getIcon(),
            moveDownPlugIn.createEnableCheck(), context);
    }

    private void addPopupMenuItem(PlugIn plugIn, boolean checkBox, Icon icon, EnableCheck enableCheck, WorkbenchContext context) {
        FeatureInstaller installer = new FeatureInstaller(context);
        installer.addPopupMenuItem(popupMenu,
        addCleanUp(plugIn),
        plugIn.getName(), checkBox, icon, enableCheck);
    }

    private PlugIn addCleanUp(final PlugIn plugIn) {
        return new PlugIn() {
            public String toString() {
                return plugIn.toString();
            }
            public boolean execute(PlugInContext context) throws Exception {
                try {
                return plugIn.execute(context);
                }
                finally {
                    setCurrentClickPoint(null);
                    updateComponents();
                }
            }
            public void initialize(PlugInContext context) throws Exception {
                plugIn.initialize(context);
            }
            public String getName() {
                return plugIn.getName();
            }
        };
    }

    public boolean isModified() {
        return modified;
    }

    private Collection toFields(int[] rows) {
        ArrayList fields = new ArrayList();

        for (int i = 0; i < rows.length; i++) {
            fields.add(getModel().get(rows[i]));
        }

        return fields;
    }

    private void updateComponents() {
        table.setShowGrid(layer.isEditable());
        table.setRowHeight(20); // fix proposed by uwe to have readable comboboxes with MetalL&F
        applyButton.setEnabled(layer.isEditable());
        revertButton.setEnabled(layer.isEditable());
        forceInvalidConversionsToNullCheckBox.setEnabled(layer.isEditable());
        reportError(validateInput());
        toolBar.updateEnabledState();
    }

    public SchemaTableModel getModel() {
        return (SchemaTableModel) table.getModel();
    }

    private TableColumn fieldNameColumn() {
        return table.getColumnModel().getColumn(
            getModel().indexOfColumn(SchemaTableModel.FIELD_NAME_COLUMN_NAME));
    }

    private TableColumn dataTypeColumn() {
        return table.getColumnModel().getColumn(
            getModel().indexOfColumn(SchemaTableModel.DATA_TYPE_COLUMN_NAME));
    }

    private void initCellEditors() {
        fieldNameColumn().setCellEditor(new MyFieldNameEditor());
        dataTypeColumn().setCellEditor(new MyDataTypeEditor(AttributeType.allTypes().toArray()));
        fieldNameColumn().setCellRenderer(
            new StripingRenderer(table.getDefaultRenderer(String.class)));
        dataTypeColumn().setCellRenderer(new StripingRenderer(new TableCellRenderer() {
            public Component getTableCellRendererComponent(
                JTable table,
                Object value,
                boolean isSelected,
                boolean hasFocus,
                int row,
                int column) {
                return table.getDefaultRenderer(String.class).getTableCellRendererComponent(
                    table,
                    (value != null) ? capitalizeFirstLetter(value.toString()) : null,
                    isSelected,
                    hasFocus,
                    row,
                    column);
            }
        }));
        table.getModel().addTableModelListener(new TableModelListener() {
            public void tableChanged(TableModelEvent e) {
                for (int i = 0; i < table.getColumnCount(); i++) {
                    ((MyEditor) table.getColumnModel().getColumn(i).getCellEditor())
                        .cancelCellEditing();
                }
            }
        });
    }

    private String capitalizeFirstLetter(String string) {
        return string.toUpperCase().charAt(0) + string.toLowerCase().substring(1);
    }

    void jbInit() throws Exception {
        toolBar.setOrientation(JToolBar.VERTICAL);
        border1 = BorderFactory.createEtchedBorder(Color.white, new Color(148, 145, 140));
        this.setLayout(gridBagLayout2);
        jPanel1.setLayout(gridBagLayout1);
        statusLabel.setBorder(BorderFactory.createLoweredBevelBorder());
        statusLabel.setText(" ");
        applyButton.setText(I18N.get("ui.SchemaPanel.apply-changes"));
        applyButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(ActionEvent e) {
                applyButton_actionPerformed(e);
            }
        });
        forceInvalidConversionsToNullCheckBox.setToolTipText(I18N.get("ui.SchemaPanel.leave-unchecked-if-you-want-to-be-notified-of-conversion-errors"));
        forceInvalidConversionsToNullCheckBox.setText(I18N.get("ui.SchemaPanel.force-invalid-conversions-to-null"));
        buttonPanel.setLayout(gridBagLayout3);
        buttonPanel.setBorder(BorderFactory.createLoweredBevelBorder());
        revertButton.setText(I18N.get("ui.SchemaPanel.revert-changes"));
        revertButton.addActionListener(new java.awt.event.ActionListener() {
            public void actionPerformed(ActionEvent e) {
                revertButton_actionPerformed(e);
            }
        });
        jPanel3.setLayout(borderLayout1);
        this.add(
            jPanel3,
            new GridBagConstraints(
                0,
                0,
                1,
                1,
                1.0,
                1.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.BOTH,
                new Insets(0, 0, 0, 0),
                0,
                0));
        jPanel3.add(toolBar, BorderLayout.WEST);
        jPanel3.add(jScrollPane1, BorderLayout.CENTER);
        jScrollPane1.getViewport().add(table, null);
        this.add(
            jPanel1,
            new GridBagConstraints(
                0,
                3,
                1,
                1,
                0.0,
                0.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.HORIZONTAL,
                new Insets(0, 0, 0, 0),
                0,
                0));
        jPanel1.add(
            statusLabel,
            new GridBagConstraints(
                0,
                0,
                1,
                1,
                1.0,
                0.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.HORIZONTAL,
                new Insets(0, 0, 0, 0),
                0,
                0));
        this.add(
            buttonPanel,
            new GridBagConstraints(
                0,
                2,
                1,
                1,
                1.0,
                0.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.HORIZONTAL,
                new Insets(0, 0, 0, 0),
                0,
                0));
        buttonPanel.add(
            applyButton,
            new GridBagConstraints(
                1,
                1,
                1,
                1,
                0.0,
                0.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.NONE,
                new Insets(4, 4, 4, 4),
                0,
                0));
        buttonPanel.add(
            forceInvalidConversionsToNullCheckBox,
            new GridBagConstraints(
                4,
                1,
                1,
                1,
                0.0,
                0.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.NONE,
                new Insets(0, 0, 0, 0),
                0,
                0));
        buttonPanel.add(
            jPanel2,
            new GridBagConstraints(
                3,
                1,
                1,
                1,
                1.0,
                0.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.HORIZONTAL,
                new Insets(0, 0, 0, 0),
                0,
                0));
        buttonPanel.add(
            revertButton,
            new GridBagConstraints(
                2,
                1,
                1,
                1,
                0.0,
                0.0,
                GridBagConstraints.CENTER,
                GridBagConstraints.NONE,
                new Insets(0, 0, 0, 4),
                0,
                0));
    }

    private void reportError(String message) {
        if (message != null) {
            statusLabel.setText(message);
            statusLabel.setIcon(GUIUtil.toSmallIcon(IconLoader.icon("Delete.gif")));
        } else {
            statusLabel.setText(" ");
            statusLabel.setIcon(null);
        }
    }

    private int geometryCount() {
        int geometryCount = 0;

        for (int i = 0; i < getModel().getRowCount(); i++) {
            if (getModel().get(i).getType() == AttributeType.GEOMETRY) {
                geometryCount++;
            }
        }

        return geometryCount;
    }

    public String validateInput() {
        for (int i = 0; i < table.getColumnCount(); i++) {
            String error =
                ((MyEditor) table.getColumnModel().getColumn(i).getCellEditor())
                    .getCurrentErrorMessage();

            if (error != null) {
                return error;
            }
        }

        if (geometryCount() > 1) {
            return I18N.get("ui.SchemaPanel.only-one-geometry-field-is-allowed");
        }

        if (geometryCount() == 0) {
            return I18N.get("ui.SchemaPanel.a-geometry-field-must-be-defined");
        }

        return null;
    }

    private String validate(int row, AttributeType type) {
        if (type == AttributeType.GEOMETRY) {
            for (int i = 0; i < getModel().getRowCount(); i++) {
                if (i == row) {
                    continue;
                }

                if (getModel().get(i).getType() == null) {
                    //One of the blank rows. [Jon Aquino]
                    continue;
                }
            }
        }

        return null;
    }

    private String validate(int row, String name) {
        if (name.trim().length() == 0) {
            return I18N.get("ui.SchemaPanel.field-name-cannot-be-blank");
        }

        //Existing fields are already trimmed. [Jon Aquino]
        for (int i = 0; i < getModel().getRowCount(); i++) {
            if (i == row) {
                continue;
            }

            if (getModel().get(i).getName() == null) {
                //One of the blank rows. [Jon Aquino]
                continue;
            }

            if (getModel().get(i).getName().equalsIgnoreCase(name.trim())) {
                return I18N.get("ui.SchemaPanel.field-name-already-exists")+": " + name;
            }
        }

        return null;
    }

    private void markAsModified() {
        modified = true;
    }

    public void markAsUnmodified() {
        modified = false;
    }

    public JTable getTable() {
        return table;
    }

    void applyButton_actionPerformed(ActionEvent e) {
        fireActionPerformed();
    }

    public void add(ActionListener l) {
        listeners.add(l);
    }

    private void fireActionPerformed() {
        for (Iterator i = listeners.iterator(); i.hasNext();) {
            ActionListener l = (ActionListener) i.next();
            l.actionPerformed(null);
        }
    }

    public boolean isForcingInvalidConversionsToNull() {
        return forceInvalidConversionsToNullCheckBox.isSelected();
    }

    public void move(Collection fieldsToMove, int displacement) {
        //Use rows-to-act-on, not selected row, because no rows may be selected
        //i.e. we might just be operating on the row the user right-clicked on. [Jon Aquino]
        int guaranteedVisibleRow = displacement > 0 ? max(rowsToActOn()) : min(rowsToActOn());
        guaranteedVisibleRow += displacement;
        //Compute guaranteedVisibleRow before doing the move, because after the
        //move the selection would have moved, or if a row were merely clicked and not
        //selected, its click point would *not* have moved -- would tricky to compute it
        //after the move! [Jon Aquino]

        ArrayList selectedFields = new ArrayList();
        int[] selectedRows = table.getSelectedRows();

        for (int i = 0; i < selectedRows.length; i++) {
            selectedFields.add(getModel().get(selectedRows[i]));
        }

        getModel().move(fieldsToMove, displacement);
        table.clearSelection();

        for (Iterator i = selectedFields.iterator(); i.hasNext();) {
            SchemaTableModel.Field field = (SchemaTableModel.Field) i.next();
            table.addRowSelectionInterval(getModel().indexOf(field), getModel().indexOf(field));
        }
        Rectangle r = table.getCellRect(guaranteedVisibleRow, 0, true);
        table.scrollRectToVisible(r);
    }

    private int min(int[] ints) {
        int min = ints[0];
        for (int i = 0; i < ints.length; i++) {
            min = Math.min(min, ints[i]);
        }
        return min;
    }

    private int max(int[] ints) {
        int max = ints[0];
        for (int i = 0; i < ints.length; i++) {
            max = Math.max(max, ints[i]);
        }
        return max;
    }


    private class StripingRenderer implements TableCellRenderer {
        private TableCellRenderer originalRenderer;

        //Row-stripe colour recommended in
        //Java Look and Feel Design Guidelines: Advanced Topics [Jon Aquino]
        private final Color LIGHT_GRAY = new Color(230, 230, 230);

        public StripingRenderer(TableCellRenderer originalRenderer) {
            this.originalRenderer = originalRenderer;
        }

        public Component getTableCellRendererComponent(
            JTable table,
            Object value,
            boolean isSelected,
            boolean hasFocus,
            int row,
            int column) {
            JComponent component =
                (JComponent) originalRenderer.getTableCellRendererComponent(
                    table,
                    value,
                    isSelected,
                    hasFocus,
                    row,
                    column);

            //If not editable, use row striping, as recommended in
            //Java Look and Feel Design Guidelines: Advanced Topics [Jon Aquino]
            component.setOpaque(true);

            if (!isSelected) {
                component.setForeground(Color.black);
                component.setBackground(
                    (layer.isEditable() || ((row % 2) == 0)) ? Color.white : LIGHT_GRAY);
            }

            return component;
        }
    }

    public class MyDataTypeEditor extends MyEditor {
        private AttributeType originalType;
        public MyDataTypeEditor(Object[] items) {
            super(new JComboBox(items));

            final ListCellRenderer originalRenderer = comboBox().getRenderer();
            comboBox().setRenderer(new ListCellRenderer() {
                public Component getListCellRendererComponent(
                    JList list,
                    Object value,
                    int index,
                    boolean isSelected,
                    boolean cellHasFocus) {
                    return originalRenderer.getListCellRendererComponent(
                        list,
                        (value != null) ? capitalizeFirstLetter(value.toString()) : null,
                        index,
                        isSelected,
                        cellHasFocus);
                }
            });
        }

        public Component getTableCellEditorComponent(
            JTable table,
            Object value,
            boolean isSelected,
            int row,
            int column) {
            originalType = (AttributeType) value;
            return super.getTableCellEditorComponent(table, value, isSelected, row, column);
        }

        private JComboBox comboBox() {
            return (JComboBox) getComponent();
        }
        public boolean stopCellEditing() {
            if (originalType != comboBox().getSelectedItem()) {
                markAsModified();
            }
            return super.stopCellEditing();
        }

        protected String validate() {
            return SchemaPanel.this.validate(row, (AttributeType) comboBox().getSelectedItem());
        }
    }

    public abstract class MyEditor extends DefaultCellEditor {
        protected int row;
        private String currentErrorMessage = null;

        public MyEditor(JComboBox comboBox) {
            super(comboBox);
        }

        public MyEditor(JTextField textField) {
            super(textField);
        }

        public Component getTableCellEditorComponent(
            JTable table,
            Object value,
            boolean isSelected,
            int row,
            int column) {
            this.row = row;
            ((JComponent) getComponent()).setBorder(new LineBorder(Color.black));

            return super.getTableCellEditorComponent(table, value, isSelected, row, column);
        }

        public void cancelCellEditing() {
            currentErrorMessage = null;
            updateComponents();
            super.cancelCellEditing();
        }

        public boolean stopCellEditing() {
            try {
                if (validate() != null) {
                    ((JComponent) getComponent()).setBorder(new LineBorder(Color.red));

                    return false;
                }

                return super.stopCellEditing();
            } finally {
                //Can't just call #validate at the top of this method, because other validations
                //apply to when the edit is finished (e.g. checking the number of geometries). [Jon Aquino]
                currentErrorMessage = validate();
                updateComponents();
            }
        }

        protected abstract String validate();

        public String getCurrentErrorMessage() {
            return currentErrorMessage;
        }
    }

    public class MyFieldNameEditor extends MyEditor {
        public MyFieldNameEditor() {
            super(new JTextField());
        }

        public boolean stopCellEditing() {
            if (!textField().getText().equals(originalText)) {
                markAsModified();
            }
            return super.stopCellEditing();
        }

        private String originalText;

        public Component getTableCellEditorComponent(
            JTable table,
            Object value,
            boolean isSelected,
            int row,
            int column) {
            originalText = (String) value;
            return super.getTableCellEditorComponent(table, value, isSelected, row, column);
        }

        private JTextField textField() {
            return (JTextField) getComponent();
        }

        protected String validate() {
            return SchemaPanel.this.validate(row, textField().getText());
        }
    }

    void revertButton_actionPerformed(ActionEvent e) {
        setModel(new SchemaTableModel(layer));
    }

    private void setCurrentClickPoint(Point currentClickPoint) {
        this.currentClickPoint = currentClickPoint;
    }

    private Point getCurrentClickPoint() {
        return currentClickPoint;
    }
}
